﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Xml;
using OfficeOpenXml.Drawing;
using OfficeOpenXml.Packaging.Ionic.Zlib;

namespace Masuit.Tools.Excel;

public static class ImageDetector
{
    private const float MToInch = 39.3700787F;
    private const float CmToInch = MToInch * 0.01F;
    public const float MmToInch = CmToInch * 0.1F;
    private const float HundredthThMmToInch = MmToInch * 0.01F;
    internal const float StandardDPI = 96f;

    internal struct TifIfd
    {
        public short Tag;
        public short Type;
        public int Count;
        public int ValueOffset;
    }

    /// <summary>
    /// 获取图像格式
    /// </summary>
    /// <param name="ms"></param>
    /// <returns></returns>
    public static ePictureType? GetPictureType(Stream ms)
    {
        var br = new BinaryReader(ms);
        if (IsJpg(br))
        {
            return ePictureType.Jpg;
        }
        if (IsBmp(br, out _))
        {
            return ePictureType.Bmp;
        }
        else if (IsGif(br))
        {
            return ePictureType.Gif;
        }
        else if (IsPng(br))
        {
            return ePictureType.Png;
        }
        else if (IsTif(br, out _))
        {
            return ePictureType.Tif;
        }
        else if (IsIco(br))
        {
            return ePictureType.Ico;
        }
        else if (IsWebP(br))
        {
            return ePictureType.WebP;
        }
        else if (IsEmf(br))
        {
            return ePictureType.Emf;
        }
        else if (IsWmf(br))
        {
            return ePictureType.Wmf;
        }
        else if (IsSvg(ms))
        {
            return ePictureType.Svg;
        }
        else if (IsGZip(br))
        {
            _ = ExtractImage(ToArray(ms), out ePictureType? pt);
            return pt;
        }
        return null;
    }

    /// <summary>
    ///
    /// </summary>
    /// <param name="stream"></param>
    /// <returns></returns>
    public static byte[] ToArray(Stream stream)
    {
        stream.Position = 0;
        byte[] bytes = new byte[stream.Length];
        stream.Read(bytes, 0, bytes.Length);

        // 设置当前流的位置为流的开始
        stream.Seek(0, SeekOrigin.Begin);
        return bytes;
    }

    private static bool IsGZip(BinaryReader br)
    {
        br.BaseStream.Position = 0;
        var sign = br.ReadBytes(2);
        return IsGZip(sign);
    }

    private static bool IsGZip(byte[] sign)
    {
        return sign.Length >= 2 && sign[0] == 0x1F && sign[1] == 0x8B;
    }

    internal static bool TryGetImageBounds(ePictureType pictureType, MemoryStream ms, ref double width, ref double height, out double horizontalResolution, out double verticalResolution)
    {
        width = 0;
        height = 0;
        horizontalResolution = verticalResolution = StandardDPI;
        try
        {
            ms.Seek(0, SeekOrigin.Begin);
            if (pictureType == ePictureType.Bmp && IsBmp(ms, ref width, ref height, ref horizontalResolution, ref verticalResolution))
            {
                return true;
            }
            if (pictureType == ePictureType.Jpg && IsJpg(ms, ref width, ref height, ref horizontalResolution, ref verticalResolution))
            {
                return true;
            }
            if (pictureType == ePictureType.Gif && IsGif(ms, ref width, ref height))
            {
                return true;
            }
            if (pictureType == ePictureType.Png && IsPng(ms, ref width, ref height, ref horizontalResolution, ref verticalResolution))
            {
                return true;
            }
            if (pictureType == ePictureType.Emf && IsEmf(ms, ref width, ref height, ref horizontalResolution, ref verticalResolution))
            {
                return true;
            }
            if (pictureType == ePictureType.Wmf && IsWmf(ms, ref width, ref height, ref horizontalResolution, ref verticalResolution))
            {
                return true;
            }
            else if (pictureType == ePictureType.Svg && IsSvg(ms, ref width, ref height))
            {
                return true;
            }
            else if (pictureType == ePictureType.Tif && IsTif(ms, ref width, ref height, ref horizontalResolution, ref verticalResolution))
            {
                return true;
            }
            else if (pictureType == ePictureType.WebP && IsWebP(ms, ref width, ref height, ref horizontalResolution, ref verticalResolution))
            {
                return true;
            }
            else if (pictureType == ePictureType.Ico && IsIcon(ms, ref width, ref height))
            {
                return true;
            }
            return false;
        }
        catch
        {
            return false;
        }
    }

    internal static byte[] ExtractImage(byte[] img, out ePictureType? type)
    {
        if (IsGZip(img))
        {
            try
            {
                var ms = new MemoryStream(img);
                var msOut = new MemoryStream();
                const int bufferSize = 4096;
                var buffer = new byte[bufferSize];
                using (var z = new GZipStream(ms, CompressionMode.Decompress))
                {
                    int size = 0;
                    do
                    {
                        size = z.Read(buffer, 0, bufferSize);
                        if (size > 0)
                        {
                            msOut.Write(buffer, 0, size);
                        }
                    }
                    while (size == bufferSize);
                    msOut.Position = 0;
                    var br = new BinaryReader(msOut);
                    if (IsEmf(br))
                    {
                        type = ePictureType.Emf;
                    }
                    else if (IsWmf(br))
                    {
                        type = ePictureType.Wmf;
                    }
                    else
                    {
                        type = null;
                    }
                    msOut.Position = 0;
                    return msOut.ToArray();
                }
            }
            catch
            {
                type = null;
                return img;
            }
        }
        type = null;
        return img;
    }

    private static bool IsJpg(MemoryStream ms, ref double width, ref double height, ref double horizontalResolution, ref double verticalResolution)
    {
        using (var br = new BinaryReader(ms))
        {
            if (IsJpg(br))
            {
                float xDensity = 1, yDensity = 1;
                while (ms.Position < ms.Length)
                {
                    var id = GetUInt16BigEndian(br);
                    var length = (int)GetInt16BigEndian(br);
                    switch (id)
                    {
                        case 0xFFE0:
                            var identifier = br.ReadBytes(5); //JFIF\0
                            var version = br.ReadBytes(2);
                            var unit = br.ReadByte();
                            xDensity = (int)GetInt16BigEndian(br);
                            yDensity = (int)GetInt16BigEndian(br);

                            if (unit == 1)
                            {
                                horizontalResolution = xDensity;
                                verticalResolution = yDensity;
                            }
                            else if (unit == 2)
                            {
                                horizontalResolution = xDensity * CmToInch;
                                verticalResolution = yDensity * CmToInch;
                            }

                            ms.Position += length - 14;
                            break;

                        case 0xFFE1:
                            var pos = ms.Position;
                            identifier = br.ReadBytes(6); //EXIF\0\0 or //EXIF\FF\FF
                            double w = 0, h = 0;
                            ReadTiffHeader(br, ref w, ref h, ref horizontalResolution, ref verticalResolution);
                            ms.Position = pos + length - 2;
                            break;

                        case 0xFFC0:
                        case 0xFFC1:
                        case 0xFFC2:
                            var precision = br.ReadByte(); //Bits
                            height = GetUInt16BigEndian(br);
                            width = GetUInt16BigEndian(br);
                            br.Close();
                            return true;

                        case 0xFFD9:
                            return height != 0 && width != 0;

                        default:
                            ms.Position += length - 2;
                            break;
                    }
                }
            }
            return false;
        }
    }

    private static bool IsJpg(BinaryReader br)
    {
        br.BaseStream.Position = 0;
        var sign = br.ReadBytes(2);    //FF D8
        return sign.Length >= 2 && sign[0] == 0xFF && sign[1] == 0xD8;
    }

    private static bool IsGif(MemoryStream ms, ref double width, ref double height)
    {
        using (var br = new BinaryReader(ms))
        {
            if (IsGif(br))
            {
                width = br.ReadUInt16();
                height = br.ReadUInt16();
                br.Close();
                return true;
            }
        }
        return false;
    }

    private static bool IsGif(BinaryReader br)
    {
        br.BaseStream.Seek(0, SeekOrigin.Begin);
        var b = br.ReadBytes(6);
        return b[0] == 0x47 && b[1] == 0x49 && b[2] == 0x46;    //byte 4-6 contains the version, but we don't check them here.
    }

    private static bool IsBmp(MemoryStream ms, ref double width, ref double height, ref double horizontalResolution, ref double verticalResolution)
    {
        using (var br = new BinaryReader(ms))
        {
            if (IsBmp(br, out string sign))
            {
                var size = br.ReadInt32();
                var reserved = br.ReadBytes(4);
                var offsetData = br.ReadInt32();

                //Info Header
                var ihSize = br.ReadInt32(); //Should be 40
                width = br.ReadInt32();
                height = br.ReadInt32();

                if (sign == "BM")
                {
                    br.ReadBytes(12);
                    horizontalResolution = br.ReadInt32() / MToInch;
                    verticalResolution = br.ReadInt32() / MToInch;
                }
                else
                {
                    horizontalResolution = verticalResolution = 1;
                }

                return true;
            }
        }
        return false;
    }

    internal static bool IsBmp(BinaryReader br, out string sign)
    {
        try
        {
            br.BaseStream.Seek(0, SeekOrigin.Begin);
            sign = Encoding.ASCII.GetString(br.ReadBytes(2));    //BM for a Windows bitmap
            return (sign == "BM" || sign == "BA" || sign == "CI" || sign == "CP" || sign == "IC" || sign == "PT");
        }
        catch
        {
            sign = null;
            return false;
        }
    }

    #region Ico

    private static bool IsIcon(MemoryStream ms, ref double width, ref double height)
    {
        using (var br = new BinaryReader(ms))
        {
            if (IsIco(br))
            {
                var imageCount = br.ReadInt16();
                width = br.ReadByte();
                if (width == 0) width = 256;
                height = br.ReadByte();
                if (height == 0) height = 256;
                br.Close();
                return true;
            }
            br.Close();
            return false;
        }
    }

    internal static bool IsIco(BinaryReader br)
    {
        br.BaseStream.Seek(0, SeekOrigin.Begin);
        var type0 = br.ReadInt16();
        var type1 = br.ReadInt16();
        return type0 == 0 && type1 == 1;
    }

    #endregion Ico

    #region WebP

    private static bool IsWebP(MemoryStream ms, ref double width, ref double height, ref double horizontalResolution, ref double verticalResolution)
    {
        width = height = 0;
        horizontalResolution = verticalResolution = StandardDPI * (1 + 1 / 3); //Excel seems to render webp at 1 1/3 size.
        using (var br = new BinaryReader(ms))
        {
            if (IsWebP(br))
            {
                var vp8 = Encoding.ASCII.GetString(br.ReadBytes(4));
                switch (vp8)
                {
                    case "VP8 ":
                        var b = br.ReadBytes(10);
                        var w = br.ReadInt16();
                        width = w & 0x3FFF;
                        var hScale = w >> 14;
                        var h = br.ReadInt16();
                        height = h & 0x3FFF;
                        hScale = h >> 14;
                        break;

                    case "VP8X":
                        br.ReadBytes(8);
                        b = br.ReadBytes(6);
                        width = BitConverter.ToInt32(new byte[] { b[0], b[1], b[2], 0 }, 0) + 1;
                        height = BitConverter.ToInt32(new byte[] { b[3], b[4], b[5], 0 }, 0) + 1;
                        break;

                    case "VP8L":
                        br.ReadBytes(5);
                        b = br.ReadBytes(4);
                        width = (b[0] | (b[1] & 0x3F) << 8) + 1;
                        height = (b[1] >> 6 | b[2] << 2 | (b[3] & 0x0F) << 10) + 1;
                        break;
                }
            }
        }
        return width != 0 && height != 0;
    }

    internal static bool IsWebP(BinaryReader br)
    {
        try
        {
            br.BaseStream.Seek(0, SeekOrigin.Begin);
            var riff = Encoding.ASCII.GetString(br.ReadBytes(4));
            var length = GetInt32BigEndian(br);
            var webP = Encoding.ASCII.GetString(br.ReadBytes(4));

            return riff == "RIFF" && webP == "WEBP";
        }
        catch
        {
            return false;
        }
    }

    #endregion WebP

    #region Tiff

    private static bool IsTif(MemoryStream ms, ref double width, ref double height, ref double horizontalResolution, ref double verticalResolution)
    {
        using (var br = new BinaryReader(ms))
        {
            return ReadTiffHeader(br, ref width, ref height, ref horizontalResolution, ref verticalResolution);
        }
    }

    private static bool ReadTiffHeader(BinaryReader br, ref double width, ref double height, ref double horizontalResolution, ref double verticalResolution)
    {
        var ms = br.BaseStream;
        var pos = ms.Position;
        if (IsTif(br, out bool isBigEndian, false))
        {
            var offset = GetTifInt32(br, isBigEndian);
            ms.Position = pos + offset;
            var numberOfIdf = GetTifInt16(br, isBigEndian);
            var ifds = new List<TifIfd>();
            for (int i = 0; i < numberOfIdf; i++)
            {
                var ifd = new TifIfd()
                {
                    Tag = GetTifInt16(br, isBigEndian),
                    Type = GetTifInt16(br, isBigEndian),
                    Count = GetTifInt32(br, isBigEndian),
                };
                if (ifd.Type == 1 || ifd.Type == 2 || ifd.Type == 6 || ifd.Type == 7)
                {
                    ifd.ValueOffset = br.ReadByte();
                    br.ReadBytes(3);
                }
                else if (ifd.Type == 3 || ifd.Type == 8)
                {
                    ifd.ValueOffset = GetTifInt16(br, isBigEndian);
                    br.ReadBytes(2);
                }
                else
                {
                    ifd.ValueOffset = GetTifInt32(br, isBigEndian);
                }
                ifds.Add(ifd);
            }

            int resolutionUnit = 2;
            foreach (var ifd in ifds)
            {
                switch (ifd.Tag)
                {
                    case 0x100:
                        width = ifd.ValueOffset;
                        break;

                    case 0x101:
                        height = ifd.ValueOffset;
                        break;

                    case 0x11A:
                        ms.Position = ifd.ValueOffset + pos;
                        var l1 = GetTifInt32(br, isBigEndian);
                        var l2 = GetTifInt32(br, isBigEndian);
                        horizontalResolution = l1 / l2;
                        break;

                    case 0x11B:
                        ms.Position = ifd.ValueOffset + pos;
                        l1 = GetTifInt32(br, isBigEndian);
                        l2 = GetTifInt32(br, isBigEndian);
                        verticalResolution = l1 / l2;
                        break;

                    case 0x128:
                        resolutionUnit = ifd.ValueOffset;
                        break;
                }
            }
            if (resolutionUnit == 1)
            {
                horizontalResolution *= CmToInch;
                verticalResolution *= CmToInch;
            }
        }
        return width != 0 && height != 0;
    }

    private static bool IsTif(BinaryReader br, out bool isBigEndian, bool resetPos = false)
    {
        try
        {
            if (resetPos)
            {
                br.BaseStream.Position = 0;
            }
            var b = br.ReadBytes(2);
            isBigEndian = Encoding.ASCII.GetString(b) == "MM";
            var identifier = GetTifInt16(br, isBigEndian);
            if (identifier == 42)
            {
                return true;
            }
        }
        catch
        {
            isBigEndian = false;
            return false;
        }
        return false;
    }

    private static short GetTifInt16(BinaryReader br, bool isBigEndian)
    {
        if (isBigEndian)
        {
            return GetInt16BigEndian(br);
        }
        else
        {
            return br.ReadInt16();
        }
    }

    private static int GetTifInt32(BinaryReader br, bool isBigEndian)
    {
        if (isBigEndian)
        {
            return GetInt32BigEndian(br);
        }
        else
        {
            return br.ReadInt32();
        }
    }

    #endregion Tiff

    #region Emf

    private static bool IsEmf(MemoryStream ms, ref double width, ref double height, ref double horizontalResolution, ref double verticalResolution)
    {
        using (var br = new BinaryReader(ms))
        {
            if (IsEmf(br))
            {
                var length = br.ReadInt32();
                var bounds = new int[4];
                bounds[0] = br.ReadInt32();
                bounds[1] = br.ReadInt32();
                bounds[2] = br.ReadInt32();
                bounds[3] = br.ReadInt32();
                var frame = new int[4];
                frame[0] = br.ReadInt32();
                frame[1] = br.ReadInt32();
                frame[2] = br.ReadInt32();
                frame[3] = br.ReadInt32();

                var signatureBytes = br.ReadBytes(4);
                var signature = Encoding.ASCII.GetString(signatureBytes);
                if (signature.Trim() == "EMF")
                {
                    var version = br.ReadUInt32();
                    var size = br.ReadUInt32();
                    var records = br.ReadUInt32();
                    var handles = br.ReadUInt16();
                    var reserved = br.ReadUInt16();

                    var nDescription = br.ReadUInt32();
                    var offDescription = br.ReadUInt32();
                    var nPalEntries = br.ReadUInt32();
                    var device = new uint[2];
                    device[0] = br.ReadUInt32();
                    device[1] = br.ReadUInt32();

                    var mm = new uint[2];
                    mm[0] = br.ReadUInt32();
                    mm[1] = br.ReadUInt32();

                    //Extension 1
                    var cbPixelFormat = br.ReadUInt32();
                    var offPixelFormat = br.ReadUInt32();
                    var bOpenGL = br.ReadUInt32();

                    //Extension 2
                    var hr = br.ReadUInt32();
                    var vr = br.ReadUInt32();

                    var id = br.ReadInt32();
                    var size2 = br.ReadInt32();

                    width = (bounds[2] - bounds[0] + 1);
                    height = (bounds[3] - bounds[1] + 1);

                    horizontalResolution = width / ((frame[2] - frame[0]) * HundredthThMmToInch * StandardDPI) * StandardDPI;
                    verticalResolution = height / ((frame[3] - frame[1]) * HundredthThMmToInch * StandardDPI) * StandardDPI;

                    return true;
                }
            }
        }
        return false;
    }

    private static bool IsEmf(BinaryReader br)
    {
        br.BaseStream.Position = 0;
        var type = br.ReadInt32();
        return type == 1;
    }

    #endregion Emf

    #region Wmf

    private const double PIXELS_PER_TWIPS = 1D / 15D;
    private const double DEFAULT_TWIPS = 1440D;

    private static bool IsWmf(MemoryStream ms, ref double width, ref double height, ref double horizontalResolution, ref double verticalResolution)
    {
        using (var br = new BinaryReader(ms))
        {
            if (IsWmf(br))
            {
                var HWmf = br.ReadInt16();
                var bounds = new ushort[4];
                bounds[0] = br.ReadUInt16();
                bounds[1] = br.ReadUInt16();
                bounds[2] = br.ReadUInt16();
                bounds[3] = br.ReadUInt16();

                var inch = br.ReadInt16();
                width = bounds[2] - bounds[0];
                height = bounds[3] - bounds[1];
                if (inch != 0)
                {
                    width *= (DEFAULT_TWIPS / inch) * PIXELS_PER_TWIPS;
                    height *= (DEFAULT_TWIPS / inch) * PIXELS_PER_TWIPS;
                }
                return width != 0 && height != 0;
            }
        }
        return false;
    }

    private static bool IsWmf(BinaryReader br)
    {
        br.BaseStream.Position = 0;
        var key = br.ReadUInt32();
        return key == 0x9AC6CDD7;
    }

    #endregion Wmf

    #region Png

    private static bool IsPng(MemoryStream ms, ref double width, ref double height, ref double horizontalResolution, ref double verticalResolution)
    {
        using (var br = new BinaryReader(ms))
        {
            return IsPng(br, ref width, ref height, ref horizontalResolution, ref verticalResolution);
        }
    }

    private static bool IsPng(BinaryReader br, ref double width, ref double height, ref double horizontalResolution, ref double verticalResolution, long fileEndPosition = long.MinValue)
    {
        if (IsPng(br))
        {
            if (fileEndPosition == long.MinValue)
            {
                fileEndPosition = br.BaseStream.Length;
            }
            while (br.BaseStream.Position < fileEndPosition)
            {
                var chunkType = ReadPngChunkHeader(br, out int length);
                switch (chunkType)
                {
                    case "IHDR":
                        width = GetInt32BigEndian(br);
                        height = GetInt32BigEndian(br);
                        br.ReadBytes(5); //Ignored bytes, Depth compression etc.
                        break;

                    case "pHYs":
                        horizontalResolution = GetInt32BigEndian(br);
                        verticalResolution = GetInt32BigEndian(br);
                        var unitSpecifier = br.ReadByte();
                        if (unitSpecifier == 1)
                        {
                            horizontalResolution /= MToInch;
                            verticalResolution /= MToInch;
                        }

                        br.Close();
                        return true;

                    default:
                        br.ReadBytes(length);
                        break;
                }
                var crc = br.ReadInt32();
            }
        }
        br.Close();
        return width != 0 && height != 0;
    }

    private static bool IsPng(BinaryReader br)
    {
        br.BaseStream.Position = 0;
        var signature = br.ReadBytes(8);
        return signature.SequenceEqual(new byte[] { 137, 80, 78, 71, 13, 10, 26, 10 });
    }

    private static string ReadPngChunkHeader(BinaryReader br, out int length)
    {
        length = GetInt32BigEndian(br);
        var b = br.ReadBytes(4);
        var type = Encoding.ASCII.GetString(b);
        return type;
    }

    #endregion Png

    #region Svg

    private static bool IsSvg(MemoryStream ms, ref double width, ref double height)
    {
        try
        {
            var reader = new XmlTextReader(ms);
            while (reader.Read())
            {
                if (reader.LocalName == "svg" && reader.NodeType == XmlNodeType.Element)
                {
                    var w = reader.GetAttribute("width");
                    var h = reader.GetAttribute("height");
                    var vb = reader.GetAttribute("viewBox");
                    reader.Close();
                    if (w == null || h == null)
                    {
                        if (vb == null)
                        {
                            return false;
                        }
                        var bounds = vb.Split(new char[] { ' ', ',' }, StringSplitOptions.RemoveEmptyEntries);
                        if (bounds.Length < 4)
                        {
                            return false;
                        }
                        if (string.IsNullOrEmpty(w))
                        {
                            w = bounds[2];
                        }
                        if (string.IsNullOrEmpty(h))
                        {
                            h = bounds[3];
                        }
                    }
                    width = GetSvgUnit(w);
                    if (double.IsNaN(width)) return false;
                    height = GetSvgUnit(h);
                    if (double.IsNaN(height)) return false;
                    return true;
                }
            }
            return false;
        }
        catch
        {
            return false;
        }
    }

    private static bool IsSvg(Stream ms)
    {
        try
        {
            ms.Position = 0;
            var reader = new XmlTextReader(ms);
            while (reader.Read())
            {
                if (reader.LocalName == "svg" && reader.NodeType == XmlNodeType.Element)
                {
                    return true;
                }
            }

            return false;
        }
        catch
        {
            return false;
        }
    }

    private static double GetSvgUnit(string v)
    {
        var factor = 1D;
        if (v.EndsWith("px", StringComparison.OrdinalIgnoreCase))
        {
            v = v.Substring(0, v.Length - 2);
        }
        else if (v.EndsWith("pt", StringComparison.OrdinalIgnoreCase))
        {
            factor = 1.25;
            v = v.Substring(0, v.Length - 2);
        }
        else if (v.EndsWith("pc", StringComparison.OrdinalIgnoreCase))
        {
            factor = 15;
            v = v.Substring(0, v.Length - 2);
        }
        else if (v.EndsWith("mm", StringComparison.OrdinalIgnoreCase))
        {
            factor = 3.543307;
            v = v.Substring(0, v.Length - 2);
        }
        else if (v.EndsWith("cm", StringComparison.OrdinalIgnoreCase))
        {
            factor = 35.43307;
            v = v.Substring(0, v.Length - 2);
        }
        else if (v.EndsWith("in", StringComparison.OrdinalIgnoreCase))
        {
            factor = 90;
            v = v.Substring(0, v.Length - 2);
        }
        if (double.TryParse(v, out double value))
        {
            return value * factor;
        }
        return double.NaN;
    }

    #endregion Svg

    private static ushort GetUInt16BigEndian(BinaryReader br)
    {
        var b = br.ReadBytes(2);
        return BitConverter.ToUInt16(new byte[] { b[1], b[0] }, 0);
    }

    private static short GetInt16BigEndian(BinaryReader br)
    {
        var b = br.ReadBytes(2);
        return BitConverter.ToInt16(new byte[] { b[1], b[0] }, 0);
    }

    private static int GetInt32BigEndian(BinaryReader br)
    {
        var b = br.ReadBytes(4);
        return BitConverter.ToInt32(new byte[] { b[3], b[2], b[1], b[0] }, 0);
    }
}
